package beer.coder.walawala.gui;

import beer.coder.walawala.common.config.ApplicationConfigure;
import beer.coder.walawala.common.config.ItemConfigure;
import beer.coder.walawala.common.config.TabConfigure;
import beer.coder.walawala.common.constant.AfterActionType;
import beer.coder.walawala.common.constant.IconType;
import beer.coder.walawala.common.constant.ItemType;
import beer.coder.walawala.common.exception.UIException;
import beer.coder.walawala.common.logger.Log;
import beer.coder.walawala.gui.component.EditableLabel;
import beer.coder.walawala.gui.component.ItemButton;
import beer.coder.walawala.gui.component.ItemsFlowPane;
import beer.coder.walawala.gui.component.Toast;
import beer.coder.walawala.gui.component.WaSystemTray;
import beer.coder.walawala.gui.constant.GuiConstant;
import beer.coder.walawala.gui.handler.MoveWindowHandler;
import beer.coder.walawala.gui.handler.WaProcessHandler;
import beer.coder.walawala.gui.handler.item.ItemActionHandler;
import beer.coder.walawala.gui.hook.GlobalHookUtil;
import beer.coder.walawala.gui.hook.GlobalKeyListener;
import beer.coder.walawala.gui.plugin.PluginManager;
import beer.coder.walawala.gui.util.DateUtil;
import beer.coder.walawala.gui.util.StageUtil;
import beer.coder.walawala.persist.ConfigureReader;
import beer.coder.walawala.persist.ConfigureWriter;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import mslinks.ShellLink;
import mslinks.ShellLinkException;
import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;
import org.jnativehook.keyboard.NativeKeyEvent;

/**
 * 主窗口.
 *
 * @author amath0312
 * @version 1.0
 * @date 2019-10-24 23:09
 */
public class App extends Application {

  private static final String LOG_TAG = "APP";
  private static final String PID_FILE = "data/pid.lck";

  private ApplicationConfigure configure;
  private File configureFile;
  private ConfigureWriter writer;
  private ConfigureReader reader;
  private Stage mainStage;
  private FlowPane toolbar;
  private BorderPane mainContent;
  private FlowPane tabsPane;
  private ScrollPane itemsScrollPane;
  private WaSystemTray systray;
  private WaProcessHandler processHandler;

  @Override
  public void init() throws Exception {
    //Log.enableFileHandler();
    Log.d(LOG_TAG, "initial...");
    processHandler = new WaProcessHandler(PID_FILE, 33133);
    if (processHandler.isConflict()) {
      processHandler.showExistedProcess();
      Log.d(LOG_TAG, "already exists, exit.");
      System.exit(0);
      return;
    }

    Log.d(LOG_TAG, "loading...");
    super.init();
    configureFile = new File(System.getProperty("user.dir"), "data/config.json");
    reader = new ConfigureReader(configureFile);
    configure = reader.read();
    writer = new ConfigureWriter(configureFile);

    Log.d(LOG_TAG, "regist hot-key");
    GlobalHookUtil.init();
    Log.d(LOG_TAG, "load item-actions");
    ItemActionHandler.getInstance();
    Log.d(LOG_TAG, "initial success.");
  }


  @Override
  public void start(Stage stage) throws Exception {
    this.mainStage = stage;
    try {
      Log.d(LOG_TAG, "initial process...");
      processHandler.initialProcess(this.mainStage);
      Log.d(LOG_TAG, "initial process success.");
    } catch (Exception e) {
      e.printStackTrace();
      Log.e(LOG_TAG, "initial process error, exit.", e);
      System.exit(-1);
    }

    Log.d(LOG_TAG, "init stage");
    stage = StageUtil.init(stage, configure, conf -> {
      saveConfigure();
    });

    stage.show();

    Platform.runLater(() -> {
      BorderPane borderPane = new BorderPane();

      Log.d(LOG_TAG, "init toolbar");
      buildToolbar();
      borderPane.setTop(toolbar);

      Log.d(LOG_TAG, "init main-content");
      buildMainContent();
      borderPane.setCenter(mainContent);

      Log.d(LOG_TAG, "init dropable-pane");
      this.mainStage.setScene(makeDropableScene(borderPane, itemsScrollPane));
    });

    Log.d(LOG_TAG, "init hot-key");
    GlobalKeyListener globalKeyListener =
        new GlobalKeyListener(NativeKeyEvent.VC_V, NativeKeyEvent.CTRL_L_MASK | NativeKeyEvent.ALT_L_MASK, () -> {
          StageUtil.switchStageStatus(this.mainStage);
        });
    GlobalScreen.addNativeKeyListener(globalKeyListener);
    systray = WaSystemTray.create(stage, s -> {
      GlobalScreen.removeNativeKeyListener(globalKeyListener);
      try {
        GlobalScreen.unregisterNativeHook();
      } catch (NativeHookException e) {
        e.printStackTrace();
      }
    });

    Log.d(LOG_TAG, "start success.");
  }

  private void buildToolbar() {
    toolbar = new FlowPane();
    toolbar.setAlignment(Pos.CENTER_RIGHT);
    toolbar.setHgap(GuiConstant.TOOLBAR_HGAP);
    toolbar.getStylesheets().add(this.getClass().getResource("/style/toolbar.css").toString());

    Label plugin = new Label();
    plugin.setPrefSize(GuiConstant.TOOL_BUTTON_WIDTH, GuiConstant.TOOL_BUTTON_HEIGHT);
    plugin.setTextAlignment(TextAlignment.CENTER);
    plugin.setAlignment(Pos.CENTER);
    plugin.setId("label_plugin");
    try {
      final ContextMenu pluginMenu = loadPlugins();

      plugin.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
        pluginMenu.show(plugin, Side.BOTTOM, 0.0, 0.0);
      });
    } catch (UIException e) {
      Log.e(LOG_TAG, "create plugin menu error", e);
    }
    Label addTab = new Label();
    addTab.setPrefSize(GuiConstant.TOOL_BUTTON_WIDTH, GuiConstant.TOOL_BUTTON_HEIGHT);
    addTab.setTextAlignment(TextAlignment.CENTER);
    addTab.setAlignment(Pos.CENTER);
    addTab.setId("label_add_tab");
    addTab.addEventHandler(MouseEvent.ANY, event -> {
      if (event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY
          && event.getClickCount() < 3) {
        EditableLabel newTab = newTab();
        if (this.itemsScrollPane.getContent() == null) {
          selectTab(newTab);
        }
      }
    });

    Label close = new Label();
    close.setPrefSize(GuiConstant.TOOL_BUTTON_WIDTH, GuiConstant.TOOL_BUTTON_HEIGHT);
    close.setTextAlignment(TextAlignment.CENTER);
    close.setAlignment(Pos.CENTER);
    close.setId("label_close");
    close.addEventHandler(MouseEvent.ANY, event -> {
      if (event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY
          && event.getClickCount() == 1) {
        closeWindow();
      }
    });

    toolbar.getChildren().addAll(plugin, addTab, close);
  }

  private ContextMenu loadPlugins() throws UIException {
    return PluginManager.getInstance().createContextMenu(mainStage);
  }

  private void buildMainContent() {
    mainContent = new BorderPane();

    tabsPane = buildTabsPane();
    itemsScrollPane = buildItemsScrollPane();

    loadTabs(configure.getTabs(), tabsPane, itemsScrollPane);

    mainContent.setTop(tabsPane);
    mainContent.setCenter(itemsScrollPane);
  }

  private FlowPane buildTabsPane() {
    FlowPane tabsPane = new FlowPane();
    tabsPane.setPadding(new Insets(GuiConstant.TABSPANE_PADDING));
    tabsPane.setHgap(GuiConstant.TAB_HGAP);
    tabsPane.setVgap(GuiConstant.TAB_VGAP);
    return tabsPane;
  }

  private ScrollPane buildItemsScrollPane() {
    final ScrollPane itemsScrollPane = new ScrollPane();
    itemsScrollPane.setPadding(new Insets(4));
    itemsScrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
    itemsScrollPane
        .widthProperty()
        .addListener(
            (observable, oldValue, newValue) -> {
              Pane flow = (Pane) itemsScrollPane.getContent();
              if (flow != null) {
                flow.setPrefWidth(newValue.doubleValue());
              }
            });
    itemsScrollPane
        .heightProperty()
        .addListener(
            (observable, oldValue, newValue) -> {
              Pane flow = (Pane) itemsScrollPane.getContent();
              if (flow != null) {
                flow.setPrefHeight(newValue.doubleValue());
              }
            });
    itemsScrollPane
        .contentProperty()
        .addListener(
            (observable, oldValue, newValue) -> {
              Pane flow = (Pane) newValue;
              if (flow != null) {
                flow.setPrefWidth(itemsScrollPane.getWidth());
                flow.setPrefHeight(itemsScrollPane.getHeight());
              }
            });
    return itemsScrollPane;
  }

  private void loadTabs(TabConfigure[] tabs, FlowPane tabsPane, ScrollPane itemsScrollPane) {
    for (int i = 0; i < tabs.length; i++) {
      TabConfigure tabConfigure = tabs[i];
      EditableLabel tab = loadTab(tabConfigure);
      if (i == 0) {
        itemsScrollPane.setContent((Node) tab.getUserData());
        tab.selectedStyle();
      }
    }
  }

  private EditableLabel newTab() {
    TabConfigure tabConfigure = new TabConfigure();
    tabConfigure.setName(TabConfigure.DEFALT_NAME);
    tabConfigure.setContentStyle(TabConfigure.DEFAULT_CONTENT_STYLE);
    tabConfigure.setTabSelectedStyle(TabConfigure.DEFAULT_TAB_SELECTED_STYLE);
    tabConfigure.setTabStyle(TabConfigure.DEFAULT_TAB_STYLE);
    this.configure.addTab(tabConfigure);
    saveConfigure();
    return loadTab(tabConfigure);
  }

  private EditableLabel loadTab(TabConfigure tabConfigure) {
    EditableLabel tab = new EditableLabel(tabConfigure.getName());
    tab.setConfigure(tabConfigure);
    tab.setOnTabnameChanged(tabname -> {
      saveConfigure();
    });
    tab.restoreTabStyle();
    tab.setPrefSize(GuiConstant.TAB_WIDTH, GuiConstant.TAB_HEIGHT);
    tab.setOnMouseClicked(
        event -> {
          if (event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getClickCount() == 1
              && event.getButton() == MouseButton.PRIMARY) {
            if (tab.getUserData() != itemsScrollPane.getContent()) {
              tabsPane.getChildren()
                  .forEach(
                      node -> {
                        ((EditableLabel) node).restoreTabStyle();
                      });
              tab.selectedStyle();
              itemsScrollPane.setContent((Node) tab.getUserData());
            }
          }
        });
    tabsPane.getChildren().add(tab);

    ContextMenu menu = new ContextMenu();
    MenuItem deleteMenu = new MenuItem("删除");
    deleteMenu.setOnAction(event -> {
      ListIterator<Node> it = tabsPane.getChildren().listIterator();
      while (it.hasNext()) {
        EditableLabel editableLabel = (EditableLabel) it.next();
        if (editableLabel.getConfigure() == tabConfigure) {
          it.remove();
          this.configure.remoteTab(tabConfigure);
          if (editableLabel.getUserData() == this.itemsScrollPane.getContent()) {
            if (it.hasPrevious()) {
              selectTab((EditableLabel) it.previous());
            } else if (it.hasNext()) {
              selectTab((EditableLabel) it.next());
            }
          }
          saveConfigure();
          break;
        }
      }
    });
    menu.getItems().add(deleteMenu);
    tab.setContextMenu(menu);

    ItemsFlowPane itemsPane = new ItemsFlowPane();
    itemsPane.setAlignment(Pos.TOP_LEFT);
    itemsPane.setUserData(tab);
    itemsPane.setStyle(tabConfigure.getContentStyle());
    itemsPane.addEventHandler(
        MouseEvent.ANY,
        new MoveWindowHandler(this.mainStage));
    tab.setUserData(itemsPane);

    itemsPane.addEventFilter(MouseEvent.MOUSE_PRESSED, (e) -> {
      itemsPane.setDragging(false);
      itemsPane.setFocusedItemButton(findItemsButton(e.getPickResult().getIntersectedNode()));
    });

    itemsPane.addEventFilter(MouseEvent.MOUSE_RELEASED, (e) -> {
      if (!itemsPane.isDragging()) {
        return;
      }

      ItemButton enteredItemButton = itemsPane.getEnteredItemButton();
      if (enteredItemButton != null) {
        enteredItemButton.setBorder(Border.EMPTY);
      }

      ItemButton focusedItemButton = itemsPane.getFocusedItemButton();
      if (focusedItemButton == null || focusedItemButton == enteredItemButton) {
        return;
      }

      List<ItemConfigure> itemConfigures = new LinkedList<>(Arrays.asList(tabConfigure.getItems()));

      ItemConfigure focusedItemConfigure = focusedItemButton.getItemConfigure();
      itemsPane.getChildren().remove(focusedItemButton);
      itemConfigures.remove(focusedItemConfigure);

      if (enteredItemButton == null) {
        itemsPane.getChildren().add(focusedItemButton);
        itemConfigures.add(focusedItemConfigure);
      } else {
        int idx = itemsPane.getChildren().indexOf(enteredItemButton);
        ItemConfigure enteredItemConfigure = enteredItemButton.getItemConfigure();

        if (idx >= 0) {
          itemsPane.getChildren().add(idx, focusedItemButton);
          itemConfigures.add(itemConfigures.indexOf(enteredItemConfigure), focusedItemConfigure);
        } else {
          itemsPane.getChildren().add(focusedItemButton);
          itemConfigures.add(focusedItemConfigure);
        }
      }

      tabConfigure.setItems(itemConfigures.toArray(new ItemConfigure[0]));
      this.saveConfigure();
    });

    itemsPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, (e) -> {
      ItemButton itemButton = findItemsButton(e.getPickResult().getIntersectedNode());
      if (!itemsPane.isDragging() && itemButton != null && itemButton == itemsPane.getFocusedItemButton()) {
        return;
      }

      itemsPane.setDragging(true);
      if (itemsPane.getEnteredItemButton() != null) {
        itemsPane.getEnteredItemButton().setBorder(Border.EMPTY);
      }

      itemsPane.setEnteredItemButton(itemButton);
      System.out.println("enter>" + itemButton);
      if (itemButton != null) {
        BorderStroke borderStroke = new BorderStroke(null, Color.CORNFLOWERBLUE, null, null,
            null, null, null, BorderStrokeStyle.SOLID, null, new BorderWidths(2), Insets.EMPTY);
        itemButton.setBorder(new Border(borderStroke));
      }
    });
    loadItems(tabConfigure.getItems(), tab, itemsPane);
    return tab;
  }

  private ItemButton findItemsButton(Node child) {
    if (child == null) {
      return null;
    }
    while (!ItemButton.class.isInstance(child)) {
      child = child.getParent();
      if (child == null) {
        break;
      }
    }
    return (ItemButton) child;
  }

  private void loadItems(ItemConfigure[] items, EditableLabel tab, Pane icons) {
    for (ItemConfigure item : items) {
      loadItem(item, tab.getConfigure(), icons);
    }
  }

  private void loadItem(ItemConfigure itemConfigure, TabConfigure tabConfigure, Pane parent) {
    ItemButton button = new ItemButton(itemConfigure, itemButton -> {
      if (this.configure.getAfterActionType() == AfterActionType.HIDE) {
        StageUtil.hideStage(this.mainStage);
      }
      Integer last = itemConfigure.getClicked();
      if (last == null || last.intValue() < 0) {
        last = Integer.valueOf(0);
      }
      itemConfigure.setClicked(Integer.valueOf(last.intValue() + 1));
      itemConfigure.setLastTime(DateUtil.now());
      saveConfigure();
    }, itemButton -> {
      saveConfigure();
    }, this);
    parent.getChildren().add(button);

    ContextMenu menu = new ContextMenu();

    MenuItem renameMenu = new MenuItem("重命名");
    renameMenu.setOnAction(event -> {
      button.rename();
    });
    menu.getItems().add(renameMenu);

    if (itemConfigure.getItemType() == ItemType.EXE
        || itemConfigure.getItemType() == ItemType.LINK
        || itemConfigure.getItemType() == ItemType.FILE
        || itemConfigure.getItemType() == ItemType.FOLDER) {
      MenuItem explorerMenu = new MenuItem("打开目标位置");
      explorerMenu.setOnAction(event -> {
        File exploreFile = null;
        if (itemConfigure.getItemType() == ItemType.EXE || itemConfigure.getItemType() == ItemType.LINK
            || itemConfigure.getItemType() == ItemType.FILE) {
          File file = new File(itemConfigure.getPath());
          exploreFile = file.getParentFile();
        } else if (itemConfigure.getItemType() == ItemType.FOLDER) {
          exploreFile = new File(itemConfigure.getPath());
        }

        if (exploreFile != null && exploreFile.exists()) {
          this.getHostServices().showDocument(exploreFile.getAbsolutePath());
        } else {
          if (exploreFile == null) {
            Toast.toast("目标文件为空", this.mainStage);
          } else {
            Toast.toast("目标文件不存在: " + exploreFile.getAbsolutePath(), this.mainStage);
          }
        }
      });

      menu.getItems().add(explorerMenu);
    }

    MenuItem deleteMenu = new MenuItem("删除");
    deleteMenu.setOnAction(event -> {
      parent.getChildren().remove(button);
      tabConfigure.remoteItem(itemConfigure);
    });
    menu.getItems().add(deleteMenu);

    button.setContextMenu(menu);
  }

  private Scene makeDropableScene(BorderPane root, ScrollPane itemsScrollPane) {
    Scene scene = new Scene(root);
    scene.setOnDragOver(
        (e) -> {
          if (e.getDragboard().hasFiles()) {
            e.acceptTransferModes(TransferMode.COPY_OR_MOVE);
            e.consume();
          }
        });

    scene.setOnDragDropped(
        (e) -> {
          Node intersect = e.getPickResult().getIntersectedNode();
          while (intersect != itemsScrollPane) {
            intersect = intersect.getParent();
            if (intersect == null) {
              return;
            }
          }

          if (e.getDragboard().hasFiles()) {
            Pane flowPane = (Pane) itemsScrollPane.getContent();

            e.getDragboard()
                .getFiles()
                .forEach(
                    (file) -> {
                      newItem(file, flowPane);
                    });
          }
        });

    return scene;
  }

  private void newItem(File file, Pane parent) {
    ItemConfigure itemConfigure = new ItemConfigure();
    String realpath;
    if (file.getName().endsWith(".lnk")) {
      try {
        ShellLink link = new ShellLink(file.getAbsolutePath());
        realpath = link.resolveTarget();
        itemConfigure.setItemType(ItemType.LINK);
      } catch (IOException | ShellLinkException ex) {
        realpath = file.getAbsolutePath();
        itemConfigure.setItemType(ItemType.OTHER);
      }
    } else if (file.isDirectory()) {
      realpath = file.getAbsolutePath();
      itemConfigure.setItemType(ItemType.FOLDER);
    } else if (file.isFile()) {
      realpath = file.getAbsolutePath();
      if (file.getName().toLowerCase().endsWith("exe")) {
        itemConfigure.setItemType(ItemType.EXE);
      } else {
        itemConfigure.setItemType(ItemType.FILE);
      }
    } else {
      realpath = "";
      itemConfigure.setItemType(ItemType.OTHER);
    }
    itemConfigure.setPath(realpath);
    itemConfigure.setName(file.getName());
    itemConfigure.setIconType(IconType.EXE);
    TabConfigure tabConfigure = ((EditableLabel) parent.getUserData()).getConfigure();
    tabConfigure.addItem(itemConfigure);
    loadItem(itemConfigure, tabConfigure, parent);
    saveConfigure();
  }

  private void selectTab(EditableLabel tab) {
    if (tab.getUserData() != itemsScrollPane.getContent()) {
      tabsPane.getChildren()
          .forEach(
              node -> {
                ((EditableLabel) node).restoreTabStyle();
              });
      tab.selectedStyle();
      itemsScrollPane.setContent((Node) tab.getUserData());
    }
  }

  private void selectTab(TabConfigure tabConfigure) {
    Node tab = this.tabsPane.getChildren().stream().filter(node -> {
      if (node instanceof EditableLabel) {
        return ((EditableLabel) node).getConfigure() == tabConfigure;
      }
      return false;
    }).findFirst().get();
    if (tab != null) {
      selectTab((EditableLabel) tab);
    }
  }

  private void closeWindow() {
    PluginManager.getInstance().closePlugins();
    this.mainStage.close();
  }


  private void saveConfigure() {
    try {
      Log.d(LOG_TAG, "saving...");
      this.writer.save(this.configure);
      Log.d(LOG_TAG, "ok");
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void stop() throws Exception {
    super.stop();
    PluginManager.getInstance().detachPlugins();
    saveConfigure();
    GlobalHookUtil.destroy();
  }

  public ScrollPane getItemsScrollPane() {
    return itemsScrollPane;
  }

  public static void main(String[] args) {
    Application.launch(App.class, args);
  }
}
